﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;

namespace FotoAnotSharp.Anotator
{
    /// <summary>
    /// This class serve to creating new polygon,
    /// adding more points, deleting points and contains
    /// overloaded method to testing if point is inside
    /// polygon
    /// </summary>
    public class AnotPolygon
    {
        AnotationEditor mParent;

        static Pen sPen;
        public static Pen Pen { get { return sPen; } }

        public int Id { get; private set; }
        public object Tag { get; set; }

        static AnotPolygon()
        {
            sPen = new Pen(Color.Black, 2);
            sPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
        }

        public AnotPolygon(AnotationEditor parent, int id)
        {
            mParent = parent;
            Id = id;
        }

        public int LastSelected { get; set; }

        private double? mArea = null;
        public double Area
        {
            get
            {
                if (!mArea.HasValue)
                {
                    RecoundArea();
                }

                return mArea.Value;
            }
        }

        void RecoundArea()
        {
            //Gauss' area formula / Shoelace formula
            double area = 0;
            for (int i = 0; i < mPoints.Count; i++)
            {
                area += mPoints[i].X * (mPoints[(i + 1) % mPoints.Count].Y - mPoints[(i - 1 + mPoints.Count) % mPoints.Count].Y);
            }
            area = Math.Abs(area) * 0.5;
            mArea = area;
        }

        private List<AnotPolygonPoint> mPoints = new List<AnotPolygonPoint>();
        public IEnumerable<AnotPolygonPoint> Points { get { return mPoints; } }

        public AnotPolygonPoint LastSelectedPoint { get; set; }

        /// <summary>
        /// Draws polygon onto graphic context with offset and zoom/scale
        /// </summary>
        /// <param name="gr">graphics context</param>
        /// <param name="offsetX">offset on X axis</param>
        /// <param name="offsetY">offset on Y axis</param>
        /// <param name="scale">scale/zoom</param>
        public void Draw(Graphics gr, int offsetX, int offsetY, float scale)
        {
            PointF[] toDraw;

            toDraw = mPoints.Select(t => AnotationPanel.CountPoint(t.Location, offsetX, offsetY, scale)).ToArray();

            foreach (PointF p in toDraw)
            {
                if (Selected)
                {
                    RectangleF rect = new RectangleF(p.X - AnotPolygonPoint.DRAW_SIZE, p.Y - AnotPolygonPoint.DRAW_SIZE, AnotPolygonPoint.DRAW_SIZE * 2, AnotPolygonPoint.DRAW_SIZE * 2);
                    gr.FillEllipse(Brushes.LightGray, rect);
                    gr.DrawEllipse(Pens.Black, rect.X, rect.Y, rect.Width, rect.Height);
                }
            }

            gr.DrawPolygon(sPen, toDraw);
#if DEBUG
            gr.DrawString(string.Format("Id: {1}\nArea {0}", Area.ToString("0.00"), Id), mParent.Font, Brushes.Black, toDraw[0]);
#endif
        }

        /// <summary>
        /// Adds point to certain position
        /// </summary>
        /// <param name="loc">PointF object representing location</param>
        /// <param name="refered">Object representing position where the new point should be added</param>
        public void AddPoint(PointF loc, AnotPolygonPoint refered = null)
        {
            AnotPolygonPoint p = new AnotPolygonPoint(this, loc) { };

            p.Moved += (p_Moved);
            if (refered == null)
                mPoints.Add(p);
            else
                mPoints.Insert(mPoints.IndexOf(refered) + 1, p);
        }

        void p_Moved(object sender, EventArgs e)
        {
            mArea = null;
        }

        /// <summary>
        /// Removes point from refered polygon
        /// </summary>
        /// <param name="po">AnotPolygonPoint to be removed</param>
        public void RemovePoint(AnotPolygonPoint po)
        {
            mPoints.Remove(po);
            po.Moved -= (p_Moved);
            if (mPoints.Count < 3)
                mParent.RemovePolygon(this);
        }

        public bool Selected { get; set; }

        /// <summary>
        /// Detects whether point is inside polygon
        /// </summary>
        /// <param name="p">point to be checked</param>
        public bool Contains(PointF p)
        {
            
//  Globals which should be set before calling this function:
//
//  int    polySides  =  how many corners the polygon has
//  float  polyX[]    =  horizontal coordinates of corners
//  float  polyY[]    =  vertical coordinates of corners
//  float  x, y       =  point to be tested
//
//  (Globals are used in this example for purposes of speed.  Change as
//  desired.)
//
//  The function will return YES if the point x,y is inside the polygon, or
//  NO if it is not.  If the point is exactly on the edge of the polygon,
//  then the function may return YES or NO.
//
//  Note that division by zero is avoided because the division is protected
//  by the "if" clause which surrounds it.

/*bool pointInPolygon() {

  int      i, j=polySides-1 ;
  boolean  oddNodes=NO      ;

  for (i=0; i<polySides; i++) {
    if ((polyY[i]< y && polyY[j]>=y
    ||   polyY[j]< y && polyY[i]>=y)
    &&  (polyX[i]<=x || polyX[j]<=x)) {
      if (polyX[i]+(y-polyY[i])/(polyY[j]-polyY[i])*(polyX[j]-polyX[i])<x) {
        oddNodes=!oddNodes; }}
    j=i; }

  return oddNodes; }*/

            int i = 0, j = mPoints.Count - 1;
            bool oddNodes = false;
            float x = p.X, y = p.Y;

            for (i = 0; i < mPoints.Count; i++)
            {
                if ((mPoints[i].Y < y && mPoints[j].Y >= y
                || mPoints[j].Y < y && mPoints[i].Y >= y)
                && (mPoints[i].X <= x || mPoints[j].X <= x))
                {
                    if (mPoints[i].X + (y - mPoints[i].Y) / (mPoints[j].Y - mPoints[i].Y) * (mPoints[j].X - mPoints[i].X) < x)
                    {
                        oddNodes = !oddNodes;
                    }
                }
                j = i;
            }

            return oddNodes;
        }

        /// <summary>
        /// Detects whether polygon is inside polygon
        /// </summary>
        /// <param name="polygon">polygon to be checked</param>
        public bool Contains(AnotPolygon polygon)
        {
            foreach (AnotPolygonPoint point in polygon.Points)
            {
                if (!Contains(point.Location))
                    return false;
            }

            return true;
        }

        public bool Contains(PointF p, float distance, out AnotPolygonPoint refered)
        {
            refered = null;
            AnotPolygonPoint last = mPoints.First();
            foreach (AnotPolygonPoint po in mPoints.Skip(1).Concat(new AnotPolygonPoint[] { mPoints.First() }))
            {
                if (po.GetDistance(p) <= distance)
                {
                    refered = po;
                    return true;
                }

                bool inside;
                if (Math.Abs(GetDistance(last.Location, po.Location, p, out inside)) <= distance && inside)
                {
                    refered = last;
                    return true;
                }
                last = po;
            }
            return false;
        }

        static void MakeGeneral(PointF p1, PointF p2, out float a, out float b, out float c)
        {
            b = p1.X - p2.X;
            a = -p1.Y + p2.Y;
            //ax + by + c = 0;
            c = -(a * p1.X + b * p1.Y);
        }
        public static double GetDistance(PointF p1, PointF p2, PointF x, out bool inside)
        {
            float a, b, c;
            float a1, b1, c1;
            float a2, b2, c2;
            MakeGeneral(p1, p2, out a, out b, out c);
            MakeGeneral(p1, new PointF(p1.X + a, p1.Y + b), out a1, out b1, out c1);
            MakeGeneral(p2, new PointF(p2.X + a, p2.Y + b), out a2, out b2, out c2);

            float d1 = a1 * x.X + b1 * x.Y + c1;
            float d2 = a2 * x.X + b2 * x.Y + c2;

            inside = (d1 >= 0 && d2 <= 0) || (d1 <= 0 && d2 >= 0);

            return (a * x.X + b * x.Y + c) / (Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2)));
        }

        public override string ToString()
        {
            return string.Format("ID: {0}", Id);
        }
    }

    /// <summary>
    /// This class contains method to move with point as point of polygon
    /// </summary>
    public class AnotPolygonPoint
    {
        public PointF Location { get; private set; }
        public float X { get { return Location.X; } }
        public float Y { get { return Location.Y; } }
        public const int DRAW_SIZE = 7;

        public float GetDistance(PointF p)
        {
            return (float)Math.Sqrt(Math.Pow(p.X - X, 2) + Math.Pow(p.Y - Y, 2));
        }

        public AnotPolygon Parent { get; private set; }

        public event EventHandler Moved;

        public void RaiseMoved()
        {
            if (Moved != null)
                Moved(this, new EventArgs());
        }

        public void Move(float x, float y)
        {
            Location = new PointF(X + x, Y + y);
        }

        public AnotPolygonPoint(AnotPolygon parent, PointF loc)
        {
            Parent = parent;
            Location = loc;
        }
    }

    /// <summary>
    /// This class generate composition as a n-nary tree
    /// </summary>
    public class AnotPolygonTree
    {
        public AnotPolygonTree(AnotPolygon polygon)
        {
            Polygon = polygon;
        }
        public AnotPolygon Polygon { get; private set; }
        private HashSet<AnotPolygonTree> mNodes = new HashSet<AnotPolygonTree>();
        public IEnumerable<AnotPolygonTree> Nodes { get { return mNodes; } }

        public static void MakeTree(IEnumerable<AnotPolygon> polygons, out List<AnotPolygonTree> trees)
        {
#if DEBUG
            System.Diagnostics.Stopwatch w = System.Diagnostics.Stopwatch.StartNew();
#endif
            trees = new List<AnotPolygonTree>();
            foreach (AnotPolygon t1 in polygons.OrderByDescending(t => t.Area))
            {
                AnotPolygonTree tr = new AnotPolygonTree(t1);
                bool add = true;

                foreach (AnotPolygonTree t in trees)
                {
                    if (TryAdd(tr, t))
                        add = false;
                }

                if (add)
                    trees.Add(tr);
            }
#if DEBUG
            w.Stop();
            System.Diagnostics.Debug.WriteLine("Tree generated in {0}ms", w.ElapsedMilliseconds);
#endif
        }

        static bool TryAdd(AnotPolygonTree tree, AnotPolygonTree toTree)
        {
            if (!toTree.Polygon.Contains(tree.Polygon))
                return false;

            bool addToMe = true;

            foreach (AnotPolygonTree t in toTree.Nodes)
            {
                if (TryAdd(tree, t))
                    addToMe = false;
            }

            if (addToMe)
                toTree.mNodes.Add(tree);

            return true;
        }
    }
}
